﻿#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

//函数的语法形式
	//自定义函数和库函数是⼀样的，形式如下：
	//ret_type fun_name(形式参数)
	//{
	//
	//}
	// ret_type 是函数返回类型 
	// fun_name 是函数名
	//括号中放的是形式参数
	// {}括起来的是函数体
	// ret_type 是用来表示函数计算结果的类型，有时候返回类型可以是 void ，表示什么都不返回

	// fun_name 是为了方便使用函数；就像人的名字⼀样，有了名字方便称呼，
	//函数有了名字方便调用，所以函数名尽量要根据函数的功能起的有意义。

	//函数的参数就相当于，工厂中送进去的原材料，函数的参数也可以是 void ，明确表示函数没有参数。
	//如果有参数，要交代清楚参数的类型和名字，以及参数个数。

	//{}括起来的部分被称为函数体，函数体就是完成计算的过程。

//函数的举例
    //写⼀个加法函数，完成2个整型变量的加法操作。
	//函数的定义 - 创造函数出来
    //int Add(int x, int y) //如果有参数，要交代清楚参数的类型和名字，以及参数个数。
    //{
    //  int z = 0;
    //  z = x + y;
    //	return z;
    //}
    //
    //int main()
    //{
    //	int a = 0;
    //	int b = 0;
    //	scanf("%d %d", &a, &b);
    //	//计算a和b的和
    //	//函数Add被使用的场景 - 函数的调用
    //	int c = Add(a, b); //函数调用
    //	printf("c = %d\n", c);
    //
    //	return 0;
    //}

//写一个函数，在屏幕上打印hehe
    //返回类型是void，就表示函数不返回任何值
    //参数部分的void表示这个函数不接受参数
    //void Print()
    //{
    //	printf("hehe\n");
    //}
    //
    //int main()
    //{
    //	Print();
    //	return 0;
    //}

//形参和实参
//实参
    //在上面代码中，第27~32行是 Add 函数的定义，有了函数后，再第41行调用 Add 函数的。
    //我们把第41⾏调用 Add 函数时，传递给函数的参数a和b，称为实际参数，简称实参。
    //实际参数就是真实传递给函数的参数。

//形参
    //在上面代码中，第27行定义函数的时候，在函数名 Add 后的括号中写的 x 和 y ，称为形式参数，简称形参。
    //为什么叫形式参数呢？实际上，如果只是定义了 Add 函数，⽽不去调用的话， 
    // Add 函数的参数 x 和 y 只是“形式上存在”的，不会向内存申请空间，“不会真实存在”的，所以叫形式参数。
    //形式参数只有在函数被调用的过程中为了存放实参传递过来的值，才向内存申请空间，这个过程就是“形参的实例化”。

//实参和形参的关系
    //虽然我们提到了实参是传递给形参的，他们之间是有联系的，但是形参和实参各自是独立的内存空间。
    //在调试的时候可以得知，x和y确实得到了a和b的值，但是x和y的地址和a和b的地址是不⼀样的，
    //所以我们可以理解为“形参是实参的⼀份临时拷贝”。

//return 语句
    //return语句使用的注意事项：
    //1. return 后边可以是⼀个数值，也可以是一个表达式，如果是表达式则先执行表达式，再返回表达式的结果。
    //2. return 后边也可以什么都没有，直接写 return; 这种写法适合函数返回类型是void的情况。
    //3.return语句执行后，函数就“彻底返回”，后边的代码不再执行。
//void test()
//{
//    int i = 0;
//    for (i = 0; i < 10; i++)
//    {
//        if (i == 5)
//        {
//            return;     //由调试结果可知"hehe"并没有打印出来，
//                        //所以 return 语句一旦执行，函数就“彻底返回”，后边的代码不再执行。
//                        //而如果是 break 时，break只会跳出所在循环，继续执行后面的代码，故"hehe"会打印出来
//                        //更通俗的理解为 return 是比 break 更加彻底
//        }
//        printf("%d ", i);
//    }
//    printf("\n");
//    printf("hehe\n");
//}
//int main()
//{
//    test();
//    return 0;
//}

    //4.return返回的值和函数返回类型不一致，系统会自动将“返回的值隐式转换为函数的返回类型”。
//int test()
//{
//	int n = 5;
//	if (n % 2 == 1)
//		return 3.5; //此时会有警告，因为返回值与函数返回类型不一致，但会自动将返回值转换为函数返回类型，故r为3
//	else
//		return -3.5;
//}
//
//int main()
//{
//	int r = test();
//	printf("%d\n", r);
//	return 0;
//}

    //5.如果函数中存在if等分支的语句，则要保证每种情况下都有return返回，否则会出现编译错误。
//int test()
//{
//	int n = 6;
//	if (n % 2 == 1)
//		return 1;
//} //报的警告为“不是所有的控件路径都返回值”，意思是当n为6时，if里面的条件不符，但else并没有return返回，
//  //导致不知道返回什么值，出现编译错误。
//int main()
//{
//	int r = test();
//	printf("%d\n", r);
//	return 0;
//}

    //6.函数的返回类型如果不写，编译器会默认函数的返回类型是int。
    //7.函数写了返回类型，但是函数中没有使⽤return返回值，那么函数的返回值是未知的。

//数组做函数参数
    //写一个函数将一个整型数组的内容，全部置为 - 1，再写⼀个函数打印数组的内容。
//void set_arr(int arr[10], int sz)
//{
//    int i = 0;
//    for (i = 0; i < sz; i++)
//    {
//        arr[i] = -1;
//    }
//}
//    //set_arr函数要能够对数组内容进行设置，就得把数组作为参数传递给函数，
//    //同时函数内部在设置数组每个元素的时候，也得遍历数组，需要知道数组的元素个数。
//    //所以我们需要给set_arr传递2个参数，⼀个是数组，另外⼀个是数组的元素个数。
//void print_arr(int arr[10], int sz)
//{
//    int i = 0;
//    for (i = 0; i < sz; i++)
//    {
//        printf("%d ", arr[i]);
//    }
//    printf("\n");
//}
//int main()
//{
//    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//    int sz = sizeof(arr) / sizeof(arr[0]);
//    set_arr(arr, sz); //将数组内热全置为-1    
//    print_arr(arr, sz); //打印数组内容 
//    return 0;
//}

//数组传参的几个重点知识：
    //1.函数的形式参数要和函数的实参个数匹配
    //2.函数的实参是数组，形参也是可以写成数组形式的
    //3.形参如果是“⼀维数组”，数组“大小可以省略不写”。举例：void set_arr(int arr[], int sz)
    //4.形参如果是“二维数组”行可以省略，但是列不能省略
//void Print(int arr[3][5], int m, int n)
////也可以写成 void Print(int arr[][5], int m, int n)
////但不能写成 void Print(int arr[][], int m, int n)
////而且形参中数组的列必须要和主函数的实参数组列保持一样，因为形参操作的数组和实参的数组是“同⼀个数组”
//{
//    int i = 0;
//    for (i = 0; i < m; i++)
//    {
//        int j = 0;
//        for (j = 0; j < n; j++)
//        {
//            printf("%d ", arr[i][j]);
//        }
//        printf("\n");
//    }
//}
//int main()
//{
//    int arr[3][5] = { 1,2,3,4,5,2,3,4,5,6,3,4,5,6,7 };
//    //写一个函数打印数组的内容
//    Print(arr, 3, 5);
//    return 0;
//}

    //5.数组传参，形参是不会创建新的数组的
    //6.形参操作的数组和实参的数组是“同⼀个数组”
//相较于第73行实参与形参的关系中：x和y的地址和a和b的地址不⼀样
//形参的数组和实参的数组是同一个数组，即其两个的“地址是一样”的
//这也是为什么形参为“一维数组”时数组“大小可以省略不写”，以及形参为“二维数组”时行的大小可以省略

//嵌套调用和链式访问
//嵌套调用
    //嵌套调用就是函数之间的互相调用，也正是因为函数之间有效的互相调用，最后写出来了相对大型的程序。
    //计算某年某月有多少天？如果要函数实现，可以设计2个函数:
    // is_leap_year()：根据年份确定是否是闰年
    // get_days_of_month()：调用 is_leap_year 确定是否是闰年后，再根据月计算这个月的天数

//int is_leap_year(int year)
//{
//    if ((year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0))
//    {
//        return 1; //表示输入年份为闰年
//    }
//    else
//    {
//        return 0; //表示输入年份不是闰年
//    }
//}
//
//int get_days_of_month(int year, int month)
//{
//    int day[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
//                  //0 1  2  3  4  5  6  7  8  9  10 11 12
//    int d = day[month];
//    //判断是否是闰年
//    if (is_leap_year(year) == 1)
//    {
//        d += 1;
//    }
//    return d;
//}
//
//int main()
//{
//    int year = 0;
//    int month = 0;
//    scanf("%d %d", &year, &month);
//    //计算天数
//    int d = get_days_of_month(year, month);
//    printf("%d\n", d);
//    return 0;
//}
    //这⼀段代码，完成了⼀个独立的功能。代码中反应了不少的函数调用：
    // main 函数调用 scanf 、 printf 、 get_days_of_month
    // get_days_of_month 函数调用 is_leap_year

//链式访问
    //所谓链式访问就是将⼀个“函数的返回值”作为另外⼀个函数的参数，像链条⼀样将函数串起来就是函数的链式访问。
    //比如：
#include <string.h>
//int main()
//{
//    int len = strlen("abcdef"); //1.strlen求⼀个字符串的⻓度 
//    printf("%d\n", len); //2.打印⻓度  
//    return 0;
//}
//    //前面的代码完成动作写了2条语句，如果把strlen的返回值直接作为printf函数的参数，这样就是⼀个链式访问的例子了。
//int main()
//{
//    printf("%d\n", strlen("abcdef")); //链式访问  
//    return 0;
//}

    //再看⼀个有趣的代码
//int main()
//{
//    printf("%d", printf("%d", printf("%d", 43)));
//    return 0;
//}
    //上面的例子中，我们就第⼀个printf打印的是第⼆个printf的返回值，第⼆个printf打印的是第三个printf的返回值。
    //第三个 printf 打印43，在屏幕上打印2个字符，则返回2
    //第⼆个 printf 打印2，在屏幕上打印1个字符，则放回1
    //第⼀个 printf 打印1
    //所以屏幕上最终打印：4321

    //这个代码的关键是明白 printf 函数的返回值是什么：printf函数返回的是打印在屏幕上的“字符的个数”。
//int main()
//{
//    int r = printf("%d", 43); //打印 43
//    printf("\n");
//    printf("%d", r); //打印 2
//}

//函数的声明和定义
    //多个文件
    //⼀般在企业中我们写代码时候，代码可能⽐较多，不会将所有的代码都放在⼀个⽂件中；
    //我们往往会根据程序的功能，将代码拆分放在多个⽂件中。
    //⼀般情况下，函数的声明、类型的声明放在头⽂件（.h）中，函数的实现是放在源文件（.c）文件中。
//#include "add.h"
//
//int main()
//{
//    int a = 0;
//    int b = 0;
//    scanf("%d %d", &a, &b);
//    //函数调用 
//    int c = Add(a, b);
//    printf("%d\n", c);
//    return 0;
//}

//static 和 extern
    // static 和 extern 都是C语言中的关键字。
// static 是“静态的”的意思，可以用来：
    //1.修饰局部变量
    //2.修饰全局变量
    //3.修饰函数 
//1.static 修饰局部变量：
    //代码1：
//void test()
//{
//    int n = 1;
//    n++;
//    printf("%d ", n);
//}
//int main()
//{
//    int i = 0;
//    for (i = 0; i < 5; i++)
//    {
//        test();
//    }
//    return 0;
//}
//
//    //代码2：
//void test()
//{
//    //static修饰局部变量 
//    static int n = 0;
//    n++;
//    printf("%d ", n);
//}
//int main()
//{
//    int i = 0;
//    for (i = 0; i < 5; i++)
//    {
//        test();
//    }
//    return 0;
//}
    //代码1的 test 函数中的局部变量i是每次进入test函数先创建变量（生命周期开始）并赋值为1，
    //然后++，再打印，出函数的时候变量⽣命周期将要结束（释放内存）。
    //代码2中，我们从输出结果来看，i的值有累加的效果，其实 test 函数中的i创建好后，
    //出函数的时候是不会销毁的，重新进入函数也就不会重新创建变量，直接上次累积的数值继续计算。

    //结论：static 修饰局部变量改变了变量的⽣命周期，⽣命周期改变的本质是改变了变量的存储类型，
    //本来⼀个局部变量是存储在内存的栈区的，但是被 static 修饰后存储到了静态区。
    //存储在静态区的变量和全局变量是⼀样的，⽣命周期就和程序的⽣命周期⼀样了，
    //只有程序结束，变量才销毁，内存才回收。但是作用域不变的，即仍只能在局部范围内使用而不能在整个工程中使用。
    //使用建议：未来⼀个变量出了函数后，我们还想保留值，等下次进入函数继续使用，就可以使用static修饰。

//2.static 修饰全局变量：
    //代码1：
    //在add.c文件中当 int g_val = 2025; 时
extern int g_val;  // extern 是用来声明外部符号的，如果⼀个全局的符号在A文件中定义的，
int main()         // 在B文件中想使用，就可以使用 extern 进行声明，然后使用。
{
    printf("%d\n", g_val);
    return 0;
} //可以执行

//    //代码2：
//    //在add.c文件中当 static int g_val = 2025; 时
//extern int g_val;
//int main()
//{
//    printf("%d\n", g_val);
//    return 0;
//} //不可以执行
    //代码1正常，代码2在编译的时候会出现链接性错误。原因在于：
    // int g_val = 2018 为全局变量，全局变量默认是“代码外部链接属性”,在其他的.c文件中适当的声明是可以直接使用的，
    //但是被 static 修饰后这种外部链接属性就变成了内部链接属性,
    //这个变量只能在自己所在的.c文件内部使用而其他.c文件无法使用，即使声明了，也是无法正常使用的。

//3.static 修饰函数：
    //其实 static 修饰函数和 static 修饰全局变量是⼀模⼀样的，
    //一个函数在整个工程都可以使用，被static修饰后，只能在本文件内部使⽤，其他文件无法正常的链接使用了。
    //本质是因为函数默认和全局变量一样也是具有外部链接属性，具有外部链接属性，
    //使得函数在整个工程中只要适当的声明就可以被使用。
    //但是被 static 修饰后变成了内部链接属性，使得函数只能在自己所在源⽂件内部使用。

// extern 是用来“声明外部符号”的。
//    extern int g_val; //外部符号 g_val 来自于add.c文件中
//
//void test()
//{
//    printf("2: g_val = %d\n", g_val); //可以运行
//}
//
//int main()
//{
//	printf("1: g_val = %d\n", g_val); //可以运行
//	test();
//	return 0;
//}